fix(BA-6927): stop cross-multiplying usage bucket amounts and durations#12965
Draft
jopemachine wants to merge 1 commit into
Draft
fix(BA-6927): stop cross-multiplying usage bucket amounts and durations#12965jopemachine wants to merge 1 commit into
jopemachine wants to merge 1 commit into
Conversation
jopemachine
added a commit
that referenced
this pull request
Jul 20, 2026
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jopemachine
added a commit
that referenced
this pull request
Jul 20, 2026
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jopemachine
force-pushed
the
fix/BA-6927-usage-bucket-cross-product
branch
from
July 20, 2026 04:13
baf0826 to
81f0067
Compare
This was referenced Jul 20, 2026
jopemachine
force-pushed
the
fix/BA-6927-usage-bucket-cross-product
branch
15 times, most recently
from
July 24, 2026 02:53
831d257 to
61f3350
Compare
FairShareAggregator accumulated the raw resource amounts and the slice durations of a bucket separately and multiplied them afterwards, yielding (sum amount_k) * (sum duration_k) instead of sum(amount_k * duration_k). The cross product inflates a bucket by the number of kernel slices folded into it, so a user running 4 GPU kernels saw 4x their real fGPU-seconds. The factor compounds up the hierarchy, since each level aggregates the kernels of everything beneath it. The deltas the aggregator hands over now carry the per-slice products already summed, which is the only shape that fixes this: (sum amount, sum duration) does not determine sum(amount * duration), so the pairing has to survive into the delta. BucketDelta held exactly those two sums and had no other purpose, so it collapses to a plain ResourceSlot per bucket. usage_bucket_entries.amount becomes resource_usage, the name kernel_usage_records, the three bucket tables and the GraphQL and REST responses already use for this quantity, so one vocabulary runs the whole way through. The column also drops its precision limit: no fixed precision is defensible here -- a domain-level daily mem bucket runs to ~1e18 byte-seconds on a large cluster -- while PostgreSQL's unconstrained numeric has no ceiling, and dropping a typmod does not rewrite the table. duration_seconds goes too. It only ever existed so readers could reconstitute the product, nothing consulted it on its own, and it counted kernel-seconds rather than wall-clock, so keeping it would leave a column that invites the same misreading. The read paths previously summed the stored figure without combining it with duration at all, returning something 300x smaller than the resource-seconds the fair share calculator assumes (it divides by capacity * lookback_days * 86400), so scheduling priorities were computed on the wrong scale -- a second defect independent of the cross product. Existing buckets cannot be corrected in place, in either the JSONB mirror or the normalized entries, so the migration rebuilds both from kernel_usage_records, which stores per-slice products and was never affected. The oldest retained day is excluded because retention purges kernel records by period_end and may have truncated it; buckets older than the kernel record retention window keep their inflated values rather than being silently zeroed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jopemachine
force-pushed
the
fix/BA-6927-usage-bucket-cross-product
branch
from
July 24, 2026 02:59
61f3350 to
b89eb6e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
FairShareAggregatorsummed each bucket's resource amounts and slice durationsseparately, then multiplied — computing
(Σ amount) × (Σ duration)instead ofΣ(amount × duration). This inflates a bucket by the number of kernel slicesfolded into it (≈ concurrent kernel count), and the error compounds up the
hierarchy. Reported case: one user with 4 GPU kernels for a day reads
1382400fGPU-seconds instead of the correct
345600.kernel_usage_records, whichstores per-slice products, was never affected and is the rebuild source.
Σ(amount × duration)directly.BucketDelta(its only job was holding thetwo separate sums) collapses to a plain
ResourceSlot.usage_bucket_entries.amount→resource_usage, dropping itsNUMERIC(24,6)limit — a domain-level dailymembucket runs to ~1e18byte-seconds; unbounded
NUMERIChas no ceiling. Rename + typmod drop aremetadata-only (no table rewrite).
resource_usageis what the JSONB mirror andthe three bucket tables already call this, so one term runs end to end.
duration_secondsdropped — nothing read it on its own; it only existed toreconstitute the product.
summed the stored figure without multiplying by duration, so scheduling ran on
a ~300× too-small scale. Storing the product directly corrects it.
Migration
c4a91d7e05b2Existing buckets are wrong in both the JSONB mirror and the normalized entries and
cannot be fixed in place, so both are rebuilt from
kernel_usage_records:usage_bucket_entries+ the three*_usage_bucketsJSONB mirrors.*_fair_sharesis left alone — the next observer tick recomputes it from thecorrected entries.
period_end, so that day may be truncated). Buckets older than thekernel-record retention window keep their inflated values rather than being
zeroed — unrecoverable, and zeroing would destroy the only history left. The
~28-day fair-share lookback sits inside the rebuildable window.
immutable
kernel_usage_records, and overwrites (not increments) the JSONB, sore-running yields the same values.
downgrade()reverts only the column definition (the rebuilt values are thecorrect ones; the inflated originals are unrecoverable) and logs a warning that
the entries no longer match the restored schema.
The rebuild spans up to the retention window; back up before applying.
Test plan
pants teston the aggregator, resource_usage_history and retention suites —bucket assertions corrected to resource-seconds; regression parametrized over
1/2/4/10 concurrent kernels; multi-tenant tick (two projects, one user active in
both) pinning per-level factors and bucket keys
pants fmt/lint/checkclean on the changesetin-window level rebuilds to its expected value, the oldest retained day stays
untouched, buckets with no kernel records collapse to
{}, and the downgradewarning prints
Follow-ups (not in this PR)
concurrent observers could lose an update (only leader election guards it today).
period_endis not in theon_conflictupdate set, so the period-extensionstrategy described in the bucket docstrings never actually happens.
ResourceSlothas no unit notion, so an allocation and an already-integratedvalue share a type — what made this defect expressible; distinguishing them
properly spans ~15 files and belongs in its own change.
Resolves BA-6927
🤖 Generated with Claude Code